/*
 * Copyright 2016 Davide Steduto
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package eu.davidea.viewholders;

import android.animation.Animator;
import android.support.annotation.CallSuper;
import android.support.annotation.NonNull;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import java.util.List;

import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.davidea.flexibleadapter.SelectableAdapter;
import eu.davidea.flexibleadapter.helpers.AnimatorHelper;
import eu.davidea.flexibleadapter.helpers.ItemTouchHelperCallback;
import eu.davidea.flexibleadapter.items.IFlexible;

/**
 * Helper Class that implements:
 * <br/>- Single tap
 * <br/>- Long tap
 * <br/>- Touch for Drag and Swipe.
 * <p>You must extend and implement this class for the own ViewHolder.</p>
 *
 * @author Davide Steduto
 * @since 03/01/2016 Created
 * <br/>23/01/2016 ItemTouch with Drag&Drop, Swipe
 * <br/>26/01/2016 Constructor revisited
 * <br/>18/06/2016 StickyHeader flag is delegated to the super class (ContentViewHolder)
 */
public abstract class FlexibleViewHolder extends ContentViewHolder
		implements View.OnClickListener, View.OnLongClickListener,
		View.OnTouchListener, ItemTouchHelperCallback.ViewHolderCallback {

	private static final String TAG = FlexibleViewHolder.class.getSimpleName();

	// FlexibleAdapter is needed to retrieve listeners and item status
	protected final FlexibleAdapter mAdapter;

	// These 2 fields avoid double tactile feedback triggered by Android during the touch event
	// (Drag or Swipe), also assure the LongClick event is correctly fired for ActionMode if that
	// was the user intention.
	private boolean mLongClickSkipped = false;
	private boolean alreadySelected = false;

	// State for Dragging & Swiping actions
	protected int mActionState = ItemTouchHelper.ACTION_STATE_IDLE;

	/*--------------*/
	/* CONSTRUCTORS */
	/*--------------*/

	/**
	 * Default constructor.
	 *
	 * @param view    The {@link View} being hosted in this ViewHolder
	 * @param adapter Adapter instance of type {@link FlexibleAdapter}
	 * @since 5.0.0-b1
	 */
	public FlexibleViewHolder(View view, FlexibleAdapter adapter) {
		this(view, adapter, false);
	}

	/**
	 * Constructor to configure the sticky behaviour of a view.
	 * <p><b>Note:</b> StickyHeader works only if the item has been declared of type
	 * {@link eu.davidea.flexibleadapter.items.IHeader}.</p>
	 *
	 * @param view         The {@link View} being hosted in this ViewHolder
	 * @param adapter      Adapter instance of type {@link FlexibleAdapter}
	 * @param stickyHeader true if the View can be a Sticky Header, false otherwise
	 * @since 5.0.0-b7
	 */
	public FlexibleViewHolder(View view, FlexibleAdapter adapter, boolean stickyHeader) {
		super(view, adapter, stickyHeader);
		this.mAdapter = adapter;

		getContentView().setOnClickListener(this);
		getContentView().setOnLongClickListener(this);
	}

	/*--------------------------------*/
	/* CLICK LISTENERS IMPLEMENTATION */
	/*--------------------------------*/

	/**
	 * {@inheritDoc}
	 *
	 * @see #toggleActivation()
	 * @since 5.0.0-b1
	 */
	@Override
	@CallSuper
	public void onClick(View view) {
		int position = getFlexibleAdapterPosition();
		if (!mAdapter.isEnabled(position)) return;
		// Experimented that, if LongClick is not consumed, onClick is fired. We skip the
		// call to the listener in this case, which is allowed only in ACTION_STATE_IDLE.
		if (mAdapter.mItemClickListener != null && mActionState == ItemTouchHelper.ACTION_STATE_IDLE) {
			if (FlexibleAdapter.DEBUG)
				Log.v(TAG, "onClick on position " + position + " mode=" + mAdapter.getMode());
			// Get the permission to activate the View from user
			if (mAdapter.mItemClickListener.onItemClick(position)) {
				// Now toggle the activation
				toggleActivation();
			}
		}
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see #toggleActivation()
	 * @since 5.0.0-b1
	 */
	@Override
	@CallSuper
	public boolean onLongClick(View view) {
		int position = getFlexibleAdapterPosition();
		if (!mAdapter.isEnabled(position)) return false;
		if (FlexibleAdapter.DEBUG)
			Log.v(TAG, "onLongClick on position " + position + " mode=" + mAdapter.getMode());
		// If DragLongPress is enabled, then LongClick must be skipped and the listener will
		// be called in onActionStateChanged in Drag mode.
		if (mAdapter.mItemLongClickListener != null && !mAdapter.isLongPressDragEnabled()) {
			mAdapter.mItemLongClickListener.onItemLongClick(position);
			toggleActivation();
			return true;
		}
		mLongClickSkipped = true;
		return false;
	}

	/**
	 * <b>Should be used only by the Handle View!</b><br/>
	 * {@inheritDoc}
	 *
	 * @see #setDragHandleView(View)
	 * @since 5.0.0-b1
	 */
	@Override
	public boolean onTouch(View view, MotionEvent event) {
		int position = getFlexibleAdapterPosition();
		if (!mAdapter.isEnabled(position) || !isDraggable()) {
			Log.w(TAG, "Can't start drag: Item is not enabled or draggable!");
			return false;
		}
		if (FlexibleAdapter.DEBUG)
			Log.v(TAG, "onTouch with DragHandleView on position " + position + " mode=" + mAdapter.getMode());
		if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN &&
				mAdapter.isHandleDragEnabled()) {
			//Start Drag!
			mAdapter.getItemTouchHelper().startDrag(this);
		}
		return false;
	}

	/*--------------*/
	/* MAIN METHODS */
	/*--------------*/

	/**
	 * Sets the inner view which will be used to drag this itemView.
	 *
	 * @param view handle view
	 * @see #onTouch(View, MotionEvent)
	 * @since 5.0.0-b1
	 */
	@CallSuper
	@SuppressWarnings("ConstantConditions")
	protected void setDragHandleView(@NonNull View view) {
		if (view != null) view.setOnTouchListener(this);
	}

	/**
	 * Allows to change and see the activation status on the itemView and to perform animation
	 * on inner views.
	 * <p><b>IMPORTANT NOTE!</b> the selected background is visible if you added
	 * {@code android:background="?attr/selectableItemBackground"} on the item layout <u>AND</u>
	 * customized the file {@code style.xml}.</p>
	 * Alternatively, to set a background at runtime, you can use the new
	 * {@link eu.davidea.flexibleadapter.utils.DrawableUtils}.
	 * <p><b>Note:</b> This method must be called every time we want the activation state visible
	 * on the itemView, for instance: after a Click (to add the item to the selection list) or
	 * after a LongClick (to activate the ActionMode) or during dragging (to show that we enabled
	 * the Drag).</p>
	 * If you follow the above instructions, it's not necessary to invalidate this view with
	 * {@code notifyItemChanged}: In this way {@code bindViewHolder} won't be called and inner
	 * views can animate without interruptions, eventually you will see the animation running
	 * on those inner views at the same time of selection activation.
	 *
	 * @see #getActivationElevation()
	 * @since 5.0.0-b1
	 */
	@CallSuper
	public void toggleActivation() {
		// Only for selectable items
		int position = getFlexibleAdapterPosition();
		if (!mAdapter.isSelectable(position)) return;
		// [De]Activate the view
		boolean selected = mAdapter.isSelected(position);
		if (itemView.isActivated() && !selected || !itemView.isActivated() && selected) {
			itemView.setActivated(selected);
			// Apply elevation
			if (itemView.isActivated() && getActivationElevation() > 0)
				ViewCompat.setElevation(itemView, getActivationElevation());
			else if (getActivationElevation() > 0) //Leave unaltered the default elevation
				ViewCompat.setElevation(itemView, 0);
		}
	}

	/**
	 * Allows to set elevation while the view is activated.
	 * <p>Override to return desired value of elevation on this itemView.</p>
	 * <b>Note:</b> returned value must be in Pixel.
	 *
	 * @return {@code 0px} (never elevate) if not overridden
	 * @see #toggleActivation()
	 * @since 5.0.0-b2
	 */
	public float getActivationElevation() {
		return 0f;
	}

	/**
	 * Allows to activate the itemView when Swipe event occurs.
	 * <p>This method returns always false; Override with {@code "return true"} to Not expand or
	 * collapse this itemView onClick events.</p>
	 *
	 * @return always false, if not overridden
	 * @see #toggleActivation()
	 * @since 5.0.0-b2
	 */
	protected boolean shouldActivateViewWhileSwiping() {
		return false;
	}

	/**
	 * Allows to add and keep item selection if ActionMode is active.
	 * <p>This method returns always false;Override with {@code "return true"}  to add the item
	 * to the ActionMode count.</p>
	 *
	 * @return always false, if not overridden
	 * @see #toggleActivation()
	 * @since 5.0.0-b2
	 */
	protected boolean shouldAddSelectionInActionMode() {
		return false;
	}

	/*-----------*/
	/* ANIMATION */
	/*-----------*/

	/**
	 * This method is automatically called by FlexibleAdapter to animate the View while the user
	 * actively scrolls the list (forward or backward).
	 * <p>Implement your logic for different animators based on position, selection and/or
	 * direction.</p>
	 * Use can take one of the predefined Animator from {@link AnimatorHelper} or create your own
	 * {@link Animator}(s), then add it to the list of animators.
	 *
	 * @param animators NonNull list of animators, which you should add new animators
	 * @param position  can be used to differentiate the Animators based on positions
	 * @param isForward can be used to separate animation from top/bottom or from left/right scrolling
	 * @see AnimatorHelper
	 * @since 5.0.0-b8
	 */
	public void scrollAnimators(@NonNull List<Animator> animators, int position, boolean isForward) {
		// Free to implement
	}

	/*--------------------------------*/
	/* TOUCH LISTENERS IMPLEMENTATION */
	/*--------------------------------*/

	/**
	 * Here we handle the event of when the {@code ItemTouchHelper} first registers an item
	 * as being moved or swiped.
	 * <p>In this implementation, View activation is automatically handled if dragged: The Item
	 * will be added to the selection list if not selected yet and mode MULTI is activated.</p>
	 *
	 * @param position    the position of the item touched
	 * @param actionState one of {@link ItemTouchHelper#ACTION_STATE_SWIPE} or
	 *                    {@link ItemTouchHelper#ACTION_STATE_DRAG}.
	 * @see #shouldActivateViewWhileSwiping()
	 * @see #shouldAddSelectionInActionMode()
	 * @since 5.0.0-b1
	 */
	@Override
	@CallSuper
	public void onActionStateChanged(int position, int actionState) {
		mActionState = actionState;
		alreadySelected = mAdapter.isSelected(position);
		if (FlexibleAdapter.DEBUG)
			Log.v(TAG, "onActionStateChanged position=" + position + " mode=" + mAdapter.getMode() +
					" actionState=" + (actionState == ItemTouchHelper.ACTION_STATE_SWIPE ? "Swipe(1)" : "Drag(2)"));
		if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
			if (!alreadySelected) {
				// Be sure, if MODE_MULTI is active, to add this item to the selection list (call listener!)
				// Also be sure user consumes the long click event if not done in onLongClick.
				// Drag by LongPress or Drag by handleView
				if (mLongClickSkipped || mAdapter.getMode() == SelectableAdapter.MODE_MULTI) {
					// Next check, allows to initiate the ActionMode and to add selection if configured
					if ((shouldAddSelectionInActionMode() || mAdapter.getMode() != SelectableAdapter.MODE_MULTI) &&
							mAdapter.mItemLongClickListener != null && mAdapter.isSelectable(position)) {
						mAdapter.mItemLongClickListener.onItemLongClick(position);
						alreadySelected = true; //Keep selection on release!
					}
				}
				// If still not selected, be sure current item appears selected for the Drag transition
				if (!alreadySelected) {
					mAdapter.toggleSelection(position);
				}
			}
			// Now toggle the activation, Activate view and make selection visible only if necessary
			if (!itemView.isActivated()) {
				toggleActivation();
			}
		} else if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE &&
				shouldActivateViewWhileSwiping() && !alreadySelected) {
			mAdapter.toggleSelection(position);
			toggleActivation();
		}
	}

	/**
	 * Here we handle the event of when the ItemTouchHelper has completed the move or swipe.
	 * <p>In this implementation, View activation is automatically handled.</p>
	 * In case of Drag, the state will be cleared depends by current selection mode!
	 *
	 * @param position the position of the item released
	 * @see #shouldActivateViewWhileSwiping()
	 * @see #shouldAddSelectionInActionMode()
	 * @since 5.0.0-b1
	 */
	@Override
	@CallSuper
	public void onItemReleased(int position) {
		if (FlexibleAdapter.DEBUG)
			Log.v(TAG, "onItemReleased position=" + position + " mode=" + mAdapter.getMode() +
					" actionState=" + (mActionState == ItemTouchHelper.ACTION_STATE_SWIPE ? "Swipe(1)" : "Drag(2)"));
		// Be sure to keep selection if MODE_MULTI and shouldAddSelectionInActionMode is active
		if (!alreadySelected) {
			if (shouldAddSelectionInActionMode() &&
					mAdapter.getMode() == SelectableAdapter.MODE_MULTI) {
				mAdapter.mItemLongClickListener.onItemLongClick(position);
				if (mAdapter.isSelected(position)) {
					toggleActivation();
				}
			} else if (shouldActivateViewWhileSwiping() && itemView.isActivated()) {
				mAdapter.toggleSelection(position);
				toggleActivation();
			} else if (mActionState == ItemTouchHelper.ACTION_STATE_DRAG) {
				mAdapter.toggleSelection(position);
				if (itemView.isActivated()) {
					toggleActivation();
				}
			}
		}
		// Reset internal action state ready for next action
		mLongClickSkipped = false;
		mActionState = ItemTouchHelper.ACTION_STATE_IDLE;
	}

	/**
	 * @return the boolean value from the item flag, true to allow dragging
	 * @since 5.0.0-b7
	 */
	@Override
	public final boolean isDraggable() {
		IFlexible item = mAdapter.getItem(getFlexibleAdapterPosition());
		return item != null && item.isDraggable();
	}

	/**
	 * @return the boolean value from the item flag, true to allow swiping
	 * @since 5.0.0-b7
	 */
	@Override
	public final boolean isSwipeable() {
		IFlexible item = mAdapter.getItem(getFlexibleAdapterPosition());
		return item != null && item.isSwipeable();
	}

	/**
	 * @since 5.0.0-b6
	 */
	@Override
	public View getFrontView() {
		return itemView;
	}

	/**
	 * @since 5.0.0-b6
	 */
	@Override
	public View getRearLeftView() {
		return null;
	}

	/**
	 * @since 5.0.0-b6
	 */
	@Override
	public View getRearRightView() {
		return null;
	}

}